/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity.Attributes;
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace Leap.Unity.Interaction
{
///
/// A physics-enabled button. Activated by physically pressing the button, with events
/// for press and unpress.
///
public class InteractionButton : InteractionBehaviour
{
#region Inspector
[Header("UI Control")]
[Tooltip("When set to false, this UI control will not be functional. Use this instead "
+ "of disabling the component itself when you want to disable the user's "
+ "ability to affect this UI control while keeping the GameObject active and, "
+ "for example, rendering, and able to receive primaryHover state.")]
[SerializeField, FormerlySerializedAs("controlEnabled")]
private bool _controlEnabled = true;
public bool controlEnabled
{
get { return _controlEnabled; }
set { _controlEnabled = value; }
}
[Header("Motion Configuration")]
[EditTimeOnly]
public StartingPositionMode startingPositionMode = StartingPositionMode.Depressed;
public enum StartingPositionMode { Depressed, Relaxed }
///
/// The minimum and maximum heights the button can exist at.
///
[Tooltip("The minimum and maximum heights the button can exist at.")]
public Vector2 minMaxHeight = new Vector2(0f, 0.02f);
///
/// The height that this button rests at; this value is a lerp in between the min and
/// max height.
///
[Tooltip("The height that this button rests at; this value is a lerp in between the min and max height.")]
[Range(0f, 1f)]
public float restingHeight = 0.5f;
///
/// The spring force appied to the button to return it to its resting height.
///
[Range(0, 1)]
[SerializeField]
private float _springForce = 0.1f;
public float springForce
{
get
{
return _springForce;
}
set
{
_springForce = value;
}
}
#endregion
#region Events
[SerializeField]
[FormerlySerializedAs("OnPress")]
private UnityEvent _OnPress = new UnityEvent();
[SerializeField]
[FormerlySerializedAs("OnUnpress")]
private UnityEvent _OnUnpress = new UnityEvent();
public Action OnPress = () => { };
public Action OnUnpress = () => { };
#endregion
#region State
protected bool _isPressed = false;
/// Gets whether the button is currently held down.
public bool isPressed { get { return _isPressed; } }
[Obsolete("Deprecated. Use isPressed instead.", false)]
public bool isDepressed { get { return _isPressed; } }
protected bool _pressedThisFrame = false;
///
/// Gets whether the button was pressed during this Update frame.
///
public bool pressedThisFrame { get { return _pressedThisFrame; } }
[Obsolete("Deprecated. Use pressedThisFrame instead.", false)]
public bool depressedThisFrame { get { return _pressedThisFrame; } }
protected bool _unpressedThisFrame = false;
///
/// Gets whether the button was unpressed this frame.
///
public bool unpressedThisFrame { get { return _unpressedThisFrame; } }
[Obsolete("Deprecated. Use unpressedThisFrame instead.", false)]
public bool unDepressedThisFrame { get { return _unpressedThisFrame; } }
private float _pressedAmount = 0F;
///
/// Gets a normalized value between 0 and 1 based on how depressed the button
/// currently is relative to its maximum depression. 0 represents a button fully at
/// rest or pulled out beyond its resting position; 1 represents a fully-pressed
/// button.
///
public float pressedAmount { get { return _pressedAmount; } }
[Obsolete("Deprecated. Use pressedAmount instead.", false)]
public float depressedAmount { get { return _pressedAmount; } }
///
/// The initial position of this element in local space, stored on Start().
///
protected Vector3 initialLocalPosition;
///
/// The physical position of this element in local space; may diverge from the
/// graphical position.
///
protected Vector3 localPhysicsPosition;
///
/// The physical position of this element in world space; may diverge from the
/// graphical position.
///
protected Vector3 physicsPosition = Vector3.zero;
///
/// Returns the local position of this button when it is able to relax into its target
/// position.
///
public virtual Vector3 RelaxedLocalPosition
{
get
{
return initialLocalPosition
+ Vector3.back * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight);
}
}
private Rigidbody _lastDepressor;
private Vector3 _localDepressorPosition;
private Vector3 _physicsVelocity = Vector3.zero;
private bool _physicsOccurred;
private bool _initialIgnoreGrasping = false;
private Quaternion _initialLocalRotation;
private InteractionController _lockedInteractingController = null;
#endregion
#region Unity Events
void Reset()
{
contactForceMode = ContactForceMode.UI;
graspedMovementType = GraspedMovementType.Nonkinematic;
startingPositionMode = StartingPositionMode.Relaxed;
rigidbody = GetComponent();
if (rigidbody != null)
{
rigidbody.useGravity = false;
}
}
protected override void OnDisable()
{
if (isPressed)
{
_unpressedThisFrame = true;
OnUnpress();
if (_lockedInteractingController != null)
{
_lockedInteractingController.primaryHoverLocked = false;
}
}
base.OnDisable();
}
protected override void Start()
{
if (transform == transform.root)
{
Debug.LogError("This button has no parent! Please ensure that it is parented to something!", this);
enabled = false;
}
// Initialize Positions
initialLocalPosition = transform.localPosition;
if (startingPositionMode == StartingPositionMode.Relaxed)
{
initialLocalPosition = transform.localPosition
+ Vector3.forward * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight);
}
transform.localPosition = initialLocalPosition
+ Vector3.back * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight);
localPhysicsPosition = transform.localPosition;
physicsPosition = transform.position;
rigidbody.position = physicsPosition;
_initialIgnoreGrasping = ignoreGrasping;
_initialLocalRotation = transform.localRotation;
//Add a custom grasp controller
OnGraspBegin += onGraspBegin;
OnGraspEnd += onGraspEnd;
OnPress += _OnPress.Invoke;
OnUnpress += _OnUnpress.Invoke;
base.Start();
}
protected virtual void FixedUpdate()
{
if (!_physicsOccurred)
{
_physicsOccurred = true;
if (!isGrasped && !rigidbody.IsSleeping())
{
float localPhysicsDisplacementPercentage
= Mathf.InverseLerp(minMaxHeight.x, minMaxHeight.y,
initialLocalPosition.z - localPhysicsPosition.z);
// Sleep the rigidbody if it's not really moving.
if (rigidbody.position == physicsPosition
&& _physicsVelocity == Vector3.zero
&& Mathf.Abs(localPhysicsDisplacementPercentage - restingHeight) < 0.01F)
{
rigidbody.Sleep();
}
else
{
// Otherwise reset the body's position to where it was last time PhysX
// looked at it.
if (_physicsVelocity.ContainsNaN())
{
_physicsVelocity = Vector3.zero;
}
rigidbody.position = physicsPosition;
rigidbody.velocity = _physicsVelocity;
}
}
}
}
private const float FRICTION_COEFFICIENT = 30F;
private const float DRAG_COEFFICIENT = 60F;
protected virtual void Update()
{
// Reset our convenience state variables.
_pressedThisFrame = false;
_unpressedThisFrame = false;
// Disable collision on this button if it is not the primary hover.
ignoreGrasping = _initialIgnoreGrasping ? true : !isPrimaryHovered && !isGrasped;
ignoreContact = (!isPrimaryHovered || isGrasped) || !controlEnabled;
// Enforce local rotation (if button is child of non-kinematic rigidbody,
// this is necessary).
transform.localRotation = _initialLocalRotation;
// Apply physical corrections only if PhysX has modified our positions.
if (_physicsOccurred)
{
_physicsOccurred = false;
// Record and enforce the sliding state from the previous frame.
if (this.primaryHoverDistance < 0.005f || isGrasped)
{
localPhysicsPosition
= constrainDepressedLocalPosition(
transform.parent.InverseTransformPoint(rigidbody.position)
- localPhysicsPosition);
}
else
{
Vector2 localSlidePosition = new Vector2(localPhysicsPosition.x,
localPhysicsPosition.y);
localPhysicsPosition
= transform.parent.InverseTransformPoint(this.transform.position);
localPhysicsPosition = new Vector3(localSlidePosition.x,
localSlidePosition.y,
localPhysicsPosition.z);
}
// Calculate the physical kinematics of the button in local space
Vector3 localPhysicsVelocity = transform.parent.InverseTransformVector(rigidbody.velocity);
if (isPressed && isPrimaryHovered && _lastDepressor != null)
{
Vector3 curLocalDepressorPos = transform.parent.InverseTransformPoint(_lastDepressor.position);
Vector3 origLocalDepressorPos = transform.parent.InverseTransformPoint(transform.TransformPoint(_localDepressorPosition));
localPhysicsVelocity = Vector3.back * 0.05f;
localPhysicsPosition = constrainDepressedLocalPosition(curLocalDepressorPos - origLocalDepressorPos);
}
else if (isGrasped)
{
// Do nothing!
}
else
{
Vector3 originalLocalVelocity = localPhysicsVelocity;
// Spring force
localPhysicsVelocity +=
Mathf.Clamp(_springForce * 10000F
* (initialLocalPosition.z
- Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight)
- localPhysicsPosition.z),
-100f / transform.parent.lossyScale.x,
100f / transform.parent.lossyScale.x)
* Time.fixedDeltaTime * Vector3.forward;
// Friction & Drag
float velMag = originalLocalVelocity.magnitude;
var frictionDragVelocityChangeAmt = 0f;
if (velMag > 0F)
{
// Friction force
var frictionForceAmt = velMag * FRICTION_COEFFICIENT;
frictionDragVelocityChangeAmt
+= Time.fixedDeltaTime * transform.parent.lossyScale.x * frictionForceAmt;
// Drag force
float velSqrMag = velMag * velMag;
var dragForceAmt = velSqrMag * DRAG_COEFFICIENT;
frictionDragVelocityChangeAmt
+= Time.fixedDeltaTime * transform.parent.lossyScale.x * dragForceAmt;
// Apply velocity change, but don't let friction or drag let velocity
// magnitude cross zero.
var newVelMag = Mathf.Max(0, velMag - frictionDragVelocityChangeAmt);
localPhysicsVelocity = localPhysicsVelocity / velMag * newVelMag;
}
}
// Transform the local physics back into world space
physicsPosition = transform.parent.TransformPoint(localPhysicsPosition);
_physicsVelocity = transform.parent.TransformVector(localPhysicsVelocity);
// Calculate the Depression State of the Button from its Physical Position
// Set its Graphical Position to be Constrained Physically
bool oldDepressed = isPressed;
// Normalized depression amount.
_pressedAmount = localPhysicsPosition.z.Map(initialLocalPosition.z - minMaxHeight.x,
initialLocalPosition.z - Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight),
1F, 0F);
// If the button is depressed past its limit...
if (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x)
{
transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x);
if ((isPrimaryHovered && _lastDepressor != null) || isGrasped)
{
_isPressed = true;
}
else
{
physicsPosition = transform.parent.TransformPoint(new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x));
_physicsVelocity = _physicsVelocity * 0.1f;
_isPressed = false;
_lastDepressor = null;
}
// Else if the button is extended past its limit...
}
else if (localPhysicsPosition.z < initialLocalPosition.z - minMaxHeight.y)
{
transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.y);
physicsPosition = transform.position;
_isPressed = false;
_lastDepressor = null;
}
else
{
// Else, just make the physical and graphical motion of the button match
transform.localPosition = localPhysicsPosition;
// Allow some hysteresis before setting isDepressed to false.
if (!isPressed
|| !(localPhysicsPosition.z > initialLocalPosition.z - (minMaxHeight.y - minMaxHeight.x) * 0.1F))
{
_isPressed = false;
_lastDepressor = null;
}
}
// If our depression state has changed since last time...
if (isPressed && !oldDepressed)
{
primaryHoveringController.primaryHoverLocked = true;
_lockedInteractingController = primaryHoveringController;
OnPress();
_pressedThisFrame = true;
}
else if (!isPressed && oldDepressed)
{
_unpressedThisFrame = true;
OnUnpress();
if (!(isGrasped && graspingController == _lockedInteractingController))
{
_lockedInteractingController.primaryHoverLocked = false;
}
_lastDepressor = null;
}
}
}
///
/// Clamps the input local-space position to the bounds allowed by this UI element,
/// without clamping along the button depression axis. For buttons, this is locks the
/// element in local-XY space, but not along the pressing axis (Z axis).
///
protected virtual Vector3 constrainDepressedLocalPosition(Vector3 localPosition)
{
// Buttons are only allowed to move along their Z axis.
return new Vector3(
initialLocalPosition.x,
initialLocalPosition.y,
localPhysicsPosition.z + localPosition.z);
}
protected virtual void onGraspBegin()
{
primaryHoveringController.LockPrimaryHover(this);
_lockedInteractingController = primaryHoveringController;
}
protected virtual void onGraspEnd()
{
if (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x)
{
transform.localPosition = new Vector3(localPhysicsPosition.x, localPhysicsPosition.y, initialLocalPosition.z - minMaxHeight.x);
_physicsVelocity = _physicsVelocity * 0.1f;
}
if (_lockedInteractingController != null && !isPressed)
{
_lockedInteractingController.primaryHoverLocked = false;
_lockedInteractingController = null;
}
}
protected virtual void OnCollisionEnter(Collision collision) { trySetDepressor(collision.collider); }
protected virtual void OnCollisionStay(Collision collision) { trySetDepressor(collision.collider); }
// during Soft Contact, controller colliders are triggers
protected virtual void OnTriggerEnter(Collider collider) { trySetDepressor(collider); }
protected virtual void OnTriggerStay(Collider collider) { trySetDepressor(collider); }
// Try grabbing the offset between the fingertip and this object...
private void trySetDepressor(Collider collider)
{
if (collider.attachedRigidbody != null && _lastDepressor == null && (localPhysicsPosition.z > initialLocalPosition.z - minMaxHeight.x)
&& (manager.contactBoneBodies.ContainsKey(collider.attachedRigidbody)
&& !this.ShouldIgnoreHover(manager.contactBoneBodies[collider.attachedRigidbody].interactionController)))
{
_lastDepressor = collider.attachedRigidbody;
_localDepressorPosition = transform.InverseTransformPoint(collider.attachedRigidbody.position);
}
}
#endregion
#region Gizmos
protected virtual void OnDrawGizmosSelected()
{
if (transform.parent != null)
{
Gizmos.matrix = transform.parent.localToWorldMatrix;
Vector2 heights = minMaxHeight;
Vector3 originPosition = Application.isPlaying ? initialLocalPosition : transform.localPosition;
if (!Application.isPlaying && startingPositionMode == StartingPositionMode.Relaxed)
{
originPosition = transform.localPosition + Vector3.forward * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight);
}
Gizmos.color = Color.red;
Gizmos.DrawLine(originPosition + (Vector3.back * heights.x), originPosition + (Vector3.back * heights.y));
Gizmos.color = Color.green;
Gizmos.DrawLine(originPosition + (Vector3.back * heights.x), originPosition + (Vector3.back * Mathf.Lerp(heights.x, heights.y, restingHeight)));
}
}
#endregion
#region Public Methods
///
/// Sets the minimum height (x component) of the minMaxHeight property. The minimum
/// height can't be set larger than the maximum height with this method (it will be
/// clamped if necessary).
///
public void SetMinHeight(float minHeight)
{
minMaxHeight = new Vector2(Mathf.Min(minMaxHeight.y, minHeight), minMaxHeight.y);
}
[Obsolete("Deprecated. Use SetMinHeight instead.", false)]
public void setMinHeight(float minHeight)
{
SetMinHeight(minHeight);
}
///
/// Sets the maximum height (y component) of the minMaxHeight property. The maximum
/// height can't be set smaller than the minimum height with this method (it will be
/// clamped if necessary).
///
public void SetMaxHeight(float maxHeight)
{
minMaxHeight = new Vector2(minMaxHeight.x, Mathf.Max(minMaxHeight.x, maxHeight));
}
[Obsolete("Deprecated. Use SetMaxHeight instead.", false)]
public void setMaxHeight(float maxHeight)
{
SetMaxHeight(maxHeight);
}
#endregion
}
}